<?php
/**
 * @license http://www.gnu.org/licenses/lgpl.html LGPL
 * @author Harlan Iverson <h dot iverson at gmail dot com>
 * @copyright Copyright (c) 2006, Harlan Iverson.
 * @version $Id: FacesContext.class.php 22 2006-08-04 05:02:56Z Harlan $
 */
 
/**
 * LifecycleFactory is a factory object that creates (if needed) 
 * and returns Lifecycle instances. Implementations of JavaServer 
 * Faces must provide at least a default implementation of Lifecycle. 
 * Advanced implementations (or external third party libraries) 
 * MAY provide additional Lifecycle implementations (keyed by 
 * lifecycle identifiers) for performing different types of 
 * request processing on a per-request basis.
 *
 * There must be one LifecycleFactory instance per web application 
 * that is utilizing JavaServer Faces. This instance can be 
 * acquired, in a portable manner, by calling:
 *
 * PhpFaces_LifecycleFactory::GetInstance()
 * 
 * This is a deviation from the standard which uses the following method:
 * 
 * LifecycleFactory factory = (LifecycleFactory)
 * FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY);
 *
 */
class PhpFaces_LifecycleFactory {
	const DEFAULT_LIFECYCLE = "PhpFaces_LifecycleImpl";
	
	protected $lifecycles;
	
	public function __construct() {
		$this->lifecycles = array();
	}
	
	private static $instance;
	/**
	 * This currently replaces the FactoryFinder.
	 */
	public static function GetInstance() {
		if( self::$instance == null ) {
			self::$instance = new PhpFaces_LifecycleFactory();
		}
		return self::$instance;
	}
	
	
	/** Register a new Lifecycle instance, associated with the specified lifecycleId, to be supported by this LifecycleFactory. */
	public function addLifecyle( $lifecycleId, PhpFaces_Lifecycle $lifecycle ) {
		$this->lifecycles[ $lifecycleId ] = $lifecycle;
	}
	
	/** 
	 * Create (if needed) and return a Lifecycle instance for the specified lifecycle identifier. 
	 * 
	 * Each call to getLifecycle() for the same lifecycleId, from within the same web application, 
	 * must return the same Lifecycle instance.
	 */
	public function getLifecycle( $lifecycleId ) {
		
		if( !isset( $this->lifecycles[ $lifecycleId ] ) ) {
			$lifecycleClass = self::DEFAULT_LIFECYCLE;
			
			$this->lifecycles[ $lifecycleId ] = new $lifecycleClass();
		}

		return $this->lifecycles[ $lifecycleId ];
	}
	
	/** Return an Iterator over the set of lifecycle identifiers supported by this factory. */
	public function getLifecycleIds() {
		return array_keys( $this->lifecycles );
	}
}
 
/**
 * Typesafe enumeration of the legal values that may be returned by the getPhaseId() method of the FacesEvent interface.
 * @todo make type safe
 */
class PhpFaces_PhaseId {
	
	/** Don't allow instantiation */
	protected function __construct() {}

	/** Identifier that indicates an interest in events, no matter which request processing phase is being performed. */
	const ANY_PHASE = "ANY_PHASE";
	
	/** Identifier that indicates an interest in events queued for the Apply Request Values phase of the request processing lifecycle. */
	const APPLY_REQUEST_VALUES = "APPLY_REQUEST_VALUES";
	
	/** Identifier that indicates an interest in events queued for the Invoke Application phase of the request processing lifecycle. */
	const INVOKE_APPLICATION = "INVOKE_APPLICATION";
	
	/** Identifier that indicates an interest in events queued for the Process Validations phase of the request processing lifecycle. */
	const PROCESS_VALIDATIONS = "PROCESS_VALIDATIONS";
	
	/** Identifier for the Render Response phase of the request processing lifecycle. */
	const RENDER_RESPONSE = "RENDER_RESPONSE";
	
	/** Identifier that indicates an interest in events queued for the Restore View phase of the request processing lifecycle. */
	const RESTORE_VIEW = "RESTORE_VIEW";
	
	/** Identifier that indicates an interest in events queued for the Update Model Values phase of the request processing lifecycle. */
	const UPDATE_MODEL_VALUES = "UPDATE_MODEL_VALUES";
	
	/** List of valid PhaseId instances, in ascending order of their ordinal value. */
	public static function VALUES () {	
		return array(
			self::RESTORE_VIEW,
			self::PROCESS_VALIDATIONS,
			self::UPDATE_MODEL_VALUES,
			self::APPLY_REQUEST_VALUES,
			self::INVOKE_APPLICATION,
			self::RENDER_RESPONSE
		);
	}
}

/**
 * PhaseEvent represents the beginning or ending of processing for 
 * a particular phase of the request processing lifecycle, for the 
 * request encapsulated by the specified FacesContext.
 */
class PhpFaces_PhaseEvent {
	
	private $facesContext;
	private $phaseId;
	private $lifecycle;
	
	public function __construct( PhpFaces_FacesContext $facesContext, $phaseId, PhpFaces_Lifecycle $lifecycle ) {
		$this->facesContext = $facesContext;
		$this->phaseId = $phaseId;
		$this->lifecycle = $lifecycle;		
	}
	
	/** Return the FacesContext for the request being processed. */
	public function getFacesContext() {
		return $this->facesContext;	
	}
	
	/** Return the PhaseId representing the current request processing lifecycle phase. */
	public function getPhaseId() {
		return $this->phaseId;
	}
}

/**
 * An interface implemented by objects that wish to be notified at the beginning and ending of processing for each standard phase of the request processing lifecycle.
 */
interface PhpFaces_PhaseListener {
	/** Handle a notification that the processing for a particular phase of the request processing lifecycle is about to begin. */
	public function beforePhase( PhpFaces_PhaseEvent $event );
	/** Handle a notification that the processing for a particular phase has just been completed. */
	public function afterPhase( PhpFaces_PhaseEvent $event );
	/** Return the identifier of the request processing phase during which this listener is interested in processing PhaseEvent  events. */
	public function getPhaseId();
	
}

/**
 * This class encapsulates the logic used to call PhaseListeners.  It was 
 * needed because of issue 9 of the JSF 1.2 spec.  See section 11.3 for more
 * details.
 *
 * This is a direct port of the Apache MyFaces class
 *
 * @author Stan Silvert - original author
 * @author Harlan Iverson - porter
 */
class PhpFaces_PhaseListenerManager {
	private $lifecycle;
	private $facesContext;
	private $phaseListeners;
	
	/**
	 * Tracks success in the beforePhase.  Listeners that throw an exception
	 * in beforePhase or were never called because a previous listener threw
	 * an exception should not have its afterPhase called
	 */
	private $listenerSuccessMap;
	
	public function __construct( PhpFaces_Lifecycle $lifecycle, PhpFaces_FacesContext $facesContext, $phaseListeners ) {
		$this->lifecycle = $lifecycle;
		$this->facesContext = $facesContext;
		$this->phaseListeners = $phaseListeners;
		
		$this->listenerSuccessMap = array();
	}
	
	public function informPhaseListenersBefore( $phaseId ) {
		// each listener will put its outcome in this map,
		// which will later be read in by informPhaseListenersAfter
		// and used to determine if it should call that listener or not
		// (see comment on $this->listenerSuccessMap )
		$beforePhaseSuccess =& $this->listenerSuccessMap[ $phaseId ];
		$beforePhaseSuccess = array();
		
		
		$numberOfListeners = count($this->phaseListeners);
		for( $i = 0; $i < $numberOfListeners; $i++ ) {
			$phaseListener = $this->phaseListeners[ $i ];
			if( $this->isListenerForThisPhase( $phaseListener, $phaseId ) ) {
				try {
					$phaseListener->beforePhase( new PhpFaces_PhaseEvent( $this->facesContext, $phaseId, $this->lifecycle ) );
					$beforePhaseSuccess[ $i ] = true;
				} catch( Exception $e ) {
					$beforePhaseSuccess[ $i ] = false; // redundant - for clarity
					// TODO log error
				}
			}
		}
	}
	
	public function informPhaseListenersAfter( $phaseId ) {
		// each listner put its status here in informPhaseListenersBefore,
		// we will use it here... see comment there for details
		$beforePhaseSuccess =& $this->listenerSuccessMap[ $phaseId ];
		
		$numberOfListeners = count($this->phaseListeners);
		for( $i = 0; $i < $numberOfListeners; $i++ ) {
			$phaseListener = $this->phaseListeners[ $i ];
			if( $beforePhaseSuccess[ $i ] && $this->isListenerForThisPhase( $phaseListener, $phaseId ) ) {
				try {
					$phaseListener->afterPhase( new PhpFaces_PhaseEvent( $this->facesContext, $phaseId, $this->lifecycle ) );
					$beforePhaseSuccess[ $i ] = true;
				} catch( Exception $e ) {
					$beforePhaseSuccess[ $i ] = false; // redundant - for clarity
					// TODO log error
				}
			}
		}
	}
	
	protected function isListenerForThisPhase( PhpFaces_PhaseListener $phaseListener, $phaseId ) {
		$phase = $phaseListener->getPhaseId();
		return ( $phase == PhpFaces_PhaseId::ANY_PHASE || $phase == $phaseId );
	}
}

/**
 * Lifecycle manages the processing of the entire lifecycle of a particular 
 * JavaServer Faces request. It is responsible for executing all of the 
 * phases that have been defined by the JavaServer Faces Specification, 
 * in the specified order, unless otherwise directed by activities that 
 * occurred during the execution of each phase.
 * 
 * An instance of Lifecycle is created by calling the getLifecycle() 
 * method of LifecycleFactory, for a specified lifecycle identifier. 
 * Because this instance is shared across multiple simultaneous 
 * requests, it must be implemented in a thread-safe manner.
 */
interface PhpFaces_Lifecycle {
	
	/**
	 * Execute all of the phases of the request processing lifecycle, 
	 * up to but not including the Render Response phase, as described 
	 * in the JavaServer Faces Specification, in the specified order. 
	 * The processing flow can be affected (by the application, by 
	 * components, or by event listeners) by calls to the 
	 * renderResponse() or responseComplete()  methods of 
	 * the FacesContext instance associated with the current request.
	 * @param $context FacesContext for the request to be processed
	 */
	public function execute( PhpFaces_FacesContext $facesContext );
	/**
	 * Execute the Render Response phase of the request processing lifecycle, 
	 * unless the responseComplete()  method has been called on the 
	 * FacesContext instance associated with the current request.
	 * @param $context FacesContext for the request being processed
	 */
	public function render( PhpFaces_FacesContext $facesContext );
	
	
	public function addPhaseListener( PhpFaces_PhaseListener $listener );
	public function getPhaseListeners();
	public function removePhaseListener( PhpFaces_PhaseListener $listener );
	
	/**
	 * This is NOT in the standard, and may or may not 
	 * stick around... It is here simply because
	 * I need a way to put the config here.
	 * 
	 * @deprecated this is not in the spec and is around mostly for me to devel iteratively with
	 */
	public function setConfig( PhpFaces_FacesConfig $config );
	/** @deprecated see setConfig */
	public function getConfig();
}

/**
 * Based on MyFaces implementation http://svn.apache.org/viewvc/myfaces/core/trunk/impl/src/main/java/org/apache/myfaces/lifecycle/LifecycleImpl.java?revision=410138&view=markup
 */
class PhpFaces_LifecycleImpl implements PhpFaces_Lifecycle {

	private $phaseListeners;
	
	public function __construct() {
		$this->phaseListeners = array();
	}
	
	/*
	 * ===========================================================================
	 *           Lifecycle
	 * ===========================================================================
	 */
	 
	public function execute( PhpFaces_FacesContext $facesContext ) {
		
		$phaseListenerMgr = new PhpFaces_PhaseListenerManager( $this, $facesContext, $this->getPhaseListeners() );
	
		if ($this->restoreView($facesContext, $phaseListenerMgr)) {
			return;
		}
		
		if ($this->applyRequestValues($facesContext, $phaseListenerMgr)) {
			return;
		}
		
		if ($this->processValidations($facesContext, $phaseListenerMgr)) {
			return;
		}
		
		if ($this->updateModelValues($facesContext, $phaseListenerMgr)) {
			return;
		}
		
		$this->invokeApplication($facesContext, $phaseListenerMgr);
	
	}
	
	public function render( PhpFaces_FacesContext $facesContext ) {
		
		$phaseListenerMgr = new PhpFaces_PhaseListenerManager( $this, $facesContext, $this->getPhaseListeners() );
		
		try {
			$phaseListenerMgr->informPhaseListenersBefore( PhpFaces_PhaseId::RENDER_RESPONSE );
		
			// FIXME I need to read the specs and
			// find out where to put this stuff :)
			$config = $this->getConfig();
			
			//
			// Handle View
			// PhpFaces_ViewHandler
			//
			$viewHandlerClasses = $config->getViewHandlers();
			
			// use the default
			if( $viewHandlerClasses == null || count($viewHandlerClasses) == 0 ) {
				$viewHandlerClasses = array("PhpFaces_ViewHandler");
			}
			
			$viewHandler = null;
			foreach( $viewHandlerClasses as $class ) {
				$viewHandler = new $class( $viewHandler );
			}
			$viewHandler->run( $facesContext );

		} catch( Exception $e ) {
			
		}

		$phaseListenerMgr->informPhaseListenersAfter( PhpFaces_PhaseId::RENDER_RESPONSE );
	}

	
	/*
	 * ===========================================================================
	 *           Phase Implementations
	 * ===========================================================================
	 */
	 
	 
	/**
	 * Restore View (JSF.2.2.1)
	 * @return boolean true if immediate rendering should occur (generally a GET request)
	 */
	protected function restoreView( PhpFaces_FacesContext $facesContext, PhpFaces_PhaseListenerManager $phaseListenerMgr ) {
		try {
			$phaseListenerMgr->informPhaseListenersBefore( PhpFaces_PhaseId::RESTORE_VIEW );
		} catch( Exception $e ) {
			
		}

		$phaseListenerMgr->informPhaseListenersAfter( PhpFaces_PhaseId::RESTORE_VIEW );
	}
	
	/**
	 * Apply Request Values (JSF.2.2.2)
	 * @return boolean true if response is complete
	 */
	protected function applyRequestValues( PhpFaces_FacesContext $facesContext, PhpFaces_PhaseListenerManager $phaseListenerMgr ) {

		try {
			$phaseListenerMgr->informPhaseListenersBefore( PhpFaces_PhaseId::APPLY_REQUEST_VALUES );

			$isValid = false;
	
			// FIXME this should be configurable, or something
			$context = $_POST;
			
			foreach( $context as $k => $v ) {
				if( substr( $k, 0, strlen(PhpFaces_FacesContext::REQUEST_PREFIX) ) == PhpFaces_FacesContext::REQUEST_PREFIX ) {
	
					$varName = substr( $k, strlen(PhpFaces_FacesContext::REQUEST_PREFIX) );
					$varName = str_replace( PhpFaces_FacesContext::REQUEST_OBJECT_SEPERATOR, ".", $varName );
	
					$requiredSuffix = "-required";
					$isRequired = false;
	
					if( substr( $varName, -1*strlen($requiredSuffix) ) == $requiredSuffix ) {
						$varName = substr( $varName, 0, -1*strlen( $requiredSuffix ) );
						$isRequired = true;
					}
					$isEmpty = (!$v || !trim($v));
	
					if( $isRequired && !$isEmpty ) {
						$isValid = true;
						continue;
					}
					if( $varName ) {
						$varName = '#{'.$varName.'}';
					}
					$varRef =& $facesContext->resolve( $varName );
	
					$varRef = $v;
				}
			}
		} catch( Exception $e ) {
			print_r( $e );
		}

		$phaseListenerMgr->informPhaseListenersAfter( PhpFaces_PhaseId::APPLY_REQUEST_VALUES );
	}
	
	

	
	/**
	 * Process Validations (JSF.2.2.3)
	 * @return boolean true if response is complete
	 */
	protected function processValidations( PhpFaces_FacesContext $facesContext, PhpFaces_PhaseListenerManager $phaseListenerMgr ) {
		try {
			$phaseListenerMgr->informPhaseListenersBefore( PhpFaces_PhaseId::PROCESS_VALIDATIONS );
		} catch( Exception $e ) {
			
		}

		$phaseListenerMgr->informPhaseListenersAfter( PhpFaces_PhaseId::PROCESS_VALIDATIONS );
	}
	
	/**
	 * Update Model Values (JSF.2.2.4)
	 * @return boolean true if response is complete
	 */
	protected function updateModelValues( PhpFaces_FacesContext $facesContext, PhpFaces_PhaseListenerManager $phaseListenerMgr ) {
		try {
			$phaseListenerMgr->informPhaseListenersBefore( PhpFaces_PhaseId::UPDATE_MODEL_VALUES );
		} catch( Exception $e ) {
			
		}

		$phaseListenerMgr->informPhaseListenersAfter( PhpFaces_PhaseId::UPDATE_MODEL_VALUES );
	}
	
	/**
     * Invoke Application (JSF.2.2.5)
     * @return boolean true if response is complete
     */
    protected function invokeApplication(PhpFaces_FacesContext $facesContext, PhpFaces_PhaseListenerManager $phaseListenerMgr) {
    	try {
			$phaseListenerMgr->informPhaseListenersBefore( PhpFaces_PhaseId::INVOKE_APPLICATION );
		
			// FIXME I need to read the specs and
			// find out where to put this stuff :)
			$config = $this->getConfig();
			
			//
			// Handle Navigation
			// create it with support for decoration
			// PhpFaces_NavigationHandler
			//
			$navHandlerClasses = $config->getNavigationHandlers();
			
			// use the default
			if( $navHandlerClasses == null || count($navHandlerClasses) == 0 ) {
				$navHandlerClasses = array("PhpFaces_NavigationHandler");
			}
			
			$navHandler = null;
			foreach( $navHandlerClasses as $class ) {
				$navHandler = new $class( $navHandler );
			}
			$navHandler->run( $facesContext );
		
		} catch( Exception $e ) {
			
		}

		$phaseListenerMgr->informPhaseListenersAfter( PhpFaces_PhaseId::INVOKE_APPLICATION );
    }
    
    
	
	
	/*
	 * ===========================================================================
	 *           Phase Listener Management
	 * ===========================================================================
	 */
	public function addPhaseListener( PhpFaces_PhaseListener $listener ) {
		$this->phaseListeners[] = $listener;
	}
	
	public function getPhaseListeners() {
		return $this->phaseListeners;
	}
	
	public function removePhaseListener( PhpFaces_PhaseListener $listener ) {
		$key = $this->getPhaseListenerIndex( $listener );
		if( $key != null ) {
			unset( $this->phaseListeners[$key] );
		} else {
			// throw some sort of exception;
		}
	}
	
	protected function getPhaseListenerIndex( PhpFaces_PhaseListener $listener ) {
		foreach( $this->phaseListeners as $key => $iListener ) {
			if( $iListener === $listener ) {
				return $key;
			}
		}
	}
   
   
	/*
	 * ===========================================================================
	 *           Utility Functions
	 * ===========================================================================
	 */
	protected function isResponseComplete( PhpFaces_FacesContext $facesContext, $phaseId, boolean $before ) {
		if( $facesContext->getResponseComplete() ) {
			// log: "exiting from lifecycle.execute in $phaseId because getResponseComplete is true from one of the ".($before?"before":"after")." listeners.";
			return true;
		} else {
			return false;
		}
	}
	
	public function getConfig() {
		return $this->config;
	}
	public function setConfig( PhpFaces_FacesConfig $config ) {
		$this->config = $config;
	}
}

?>